﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;

using ColladaDotNet.Pipeline.BSP.Quake3;
using ColladaDotNet.Pipeline.MD3;
using ColladaDotNet.SimpleMesh;

using OpenTK;

namespace ColladaDotNet.Pipeline.BSP.Quake3
{
	public class Quake3BSPFile : IBSPFile
	{
		public string Magic;

		public int Version;

		public Direntry Entities;// 	Game-related object descriptions.
		public Direntry Textures;// 	Surface descriptions.
		public Direntry Planes;// 	Planes used by map geometry.
		public Direntry Nodes;// 	BSP tree nodes.
		public Direntry Leafs;// 	BSP tree leaves.
		public Direntry Leaffaces;// 	Lists of face indices, one list per leaf.
		public Direntry Leafbrushes;// 	Lists of brush indices, one list per leaf.
		public Direntry Models;// 	Descriptions of rigid world geometry in map.
		public Direntry Brushes;// 	Convex polyhedra used to describe solid space.
		public Direntry Brushsides;// 	Brush surfaces.
		public Direntry Vertexes;// 	Vertices used to describe faces.
		public Direntry Meshverts;// 	Lists of offsets, one list per mesh.
		public Direntry Effects;// 	List of special map effects.
		public Direntry Faces;// 	Surface geometry.
		public Direntry Lightmaps;// 	Packed lightmap data.
		public Direntry Lightvols;// 	Local illumination data.
		public Direntry Visdata;// 	Cluster-cluster visibility data. 

		public Texture[] TextureInfo;

		public Leaf[] LeafInfo;

		public int[] LeaffacesInfo;

		public int[] LeafbrushesInfo;
		public int[] MeshvertsInfo;

		public Model[] ModelInfo;

		public Face[] FaceInfo;

		public Vertex[] VertexInfo;

		public string EntitiesText;

		public Visdata VisdataInfo;

		public Plane[] PlaneInfo;

		public Node[] NodeInfo;

		private List<Set<int>> optimizedClusters;

		private class MeshVertex
		{
			public double[] TexCoords;

			public double[] Position;

			public double[] Normal;

			public Color Color;
			public MeshVertex()
			{
				
			}
			public MeshVertex(Vertex srcVert)
			{
				Position = new double[3] { srcVert.position[0], srcVert.position[1], srcVert.position[2] };
				Normal = new double[3] { srcVert.normal[0], srcVert.normal[1], srcVert.normal[2] };
				TexCoords = new double[3] { srcVert.texCoords[0], 1 - srcVert.texCoords[1], 0 };
				Color = srcVert.color;
			}
		}
		class FacesCollection
		{
			public List<Face> meshFaces = new List<Face>();
			public List<Face> polygonFaces = new List<Face>();
			public List<Face> billboardFaces = new List<Face>();
			public List<Face> patchFaces = new List<Face>();

			public List<MeshVertex> triangleStream = new List<MeshVertex>();

			public void AddPoly(Face face, Quake3BSPFile file)
			{
				polygonFaces.Add(face);

				AddFaceMeshVerts(face, file);
			}

			private void AddFaceMeshVerts(Face face, Quake3BSPFile file)
			{
				for (int i=0; i<face.n_meshverts/3; ++i)
				{
					for (int j=2; j>=0; --j)
					{
						MeshVertex item = new MeshVertex(file.VertexInfo[file.MeshvertsInfo[face.meshvert + i*3+j] + face.vertex]);
					
						triangleStream.Add(item);
					}
				}
			}

			public void AddMesh(Face face, Quake3BSPFile file)
			{
				meshFaces.Add(face);

				AddFaceMeshVerts(face, file);
			}

			public void AddPatch(Face face, Quake3BSPFile file)
			{
				patchFaces.Add(face);

				var width = face.size[0];
				var height = face.size[1];
				if (width*height != face.n_vertexes)
					throw new BSPFormatException("wrong patch point count");
			

				for (int i = 0; i < width-1; i += 1)
				{
					for (int j = 0; j < height-1; j += 1)
					{
						MeshVertex item = new MeshVertex(file.VertexInfo[face.vertex + i + j * width]);
						triangleStream.Add(item);
						item = new MeshVertex(file.VertexInfo[face.vertex + (i + 1) + (j) * width]);
						triangleStream.Add(item);
						item = new MeshVertex(file.VertexInfo[face.vertex + (i+1) + (j + 1) * width]);
						triangleStream.Add(item);

						item = new MeshVertex(file.VertexInfo[face.vertex + (i) + (j) * width]);
						triangleStream.Add(item);
						item = new MeshVertex(file.VertexInfo[face.vertex + (i+1) + (j + 1) * width]);
						triangleStream.Add(item);
						item = new MeshVertex(file.VertexInfo[face.vertex + (i) + (j+1) * width]);
						triangleStream.Add(item);
					}
				}
				//for (int i = 0; i < width-2; i+=2)
				//{
				//    for (int j = 0; j < height - 2; j += 2)
				//    {
				//        MeshVertex item = new MeshVertex(file.VertexInfo[face.vertex + i + j * width]);
				//        triangleStream.Add(item);
				//        item = new MeshVertex(file.VertexInfo[face.vertex + (i+2) + (j) * width]);
				//        triangleStream.Add(item);
				//        item = new MeshVertex(file.VertexInfo[face.vertex + (i) + (j+2) * width]);
				//        triangleStream.Add(item);

				//        item = new MeshVertex(file.VertexInfo[face.vertex + (i) + (j) * width]);
				//        triangleStream.Add(item);
				//        item = new MeshVertex(file.VertexInfo[face.vertex + (i) + (j+2) * width]);
				//        triangleStream.Add(item);
				//        item = new MeshVertex(file.VertexInfo[face.vertex + (i+2) + (j+2) * width]);
				//        triangleStream.Add(item);
				//    }
				//}
			}
		}
		private COLLADA NewColladaFile()
		{
			var doc = COLLADA.CreateNewScene("quakeBspVisual", "quakeBspPhysics");

			for (int i = 0; i < TextureInfo.Length; i++)
			{
				image img;
				if (File.Exists(TextureInfo[i].name))
					img = doc.CreateImage("texture" + i, "./" + TextureInfo[i].name);
				else if (File.Exists(TextureInfo[i].name + ".dds"))
					img = doc.CreateImage("texture" + i, "./" + TextureInfo[i].name + ".dds");
				else if (File.Exists(TextureInfo[i].name + ".jpg"))
					img = doc.CreateImage("texture" + i, "./" + TextureInfo[i].name + ".jpg");
				else if (File.Exists(TextureInfo[i].name + ".tga"))
					img = doc.CreateImage("texture" + i, "./" + TextureInfo[i].name + ".tga");
				else if (File.Exists(TextureInfo[i].name + ".png"))
					img = doc.CreateImage("texture" + i, "./" + TextureInfo[i].name + ".png");
				else
					img = doc.CreateImage("texture" + i, "./" + TextureInfo[i].name + ".jpg");

				var eff = doc.CreateEffect(img.id + "-effect", img);
				doc.CreateMaterial(img.id + "-material", img.id + "-material", eff);
				
			}
			return doc;
		}
		public COLLADA GetColladaFileSolidMesh()
		{
			var doc = NewColladaFile();

			var scene = doc.GetElementById("quakeBspVisual") as visual_scene;


			material[] materials = doc.library_materials.material;


			for (int i = 0; i < ModelInfo.Length; i++)
			{
				var model = ModelInfo[i];

				var faceByMap = new Dictionary<int, FacesCollection>();
	

				for (int f=model.face; f<model.face+model.n_faces; ++f)
				{
					var face = FaceInfo[f];
					FacesCollection faces;
					if (!faceByMap.TryGetValue(face.texture, out faces))
					{
						faces = new FacesCollection();
						faceByMap.Add(face.texture, faces);
					}
					switch (face.type)
					{
						case FaceType.polygon:
							faces.AddPoly(face,this);
							break;
						case FaceType.billboard:
							faces.billboardFaces.Add(face);
							break;
						case FaceType.patch:
							faces.AddPatch(face,this);
							break;
						case FaceType.mesh:
							faces.AddMesh(face,this);
							break;
					}
				}



				List<double> pos = new List<double>();
				List<double> nor = new List<double>();
				List<double> tex = new List<double>();
				List<double> color = new List<double>();
				foreach (var faceMap in faceByMap)
				{
					foreach (var t in faceMap.Value.triangleStream)
					{
						pos.Add(t.Position[0]);
						pos.Add(t.Position[1]);
						pos.Add(t.Position[2]);
						nor.Add(t.Normal[0]);
						nor.Add(t.Normal[1]);
						nor.Add(t.Normal[2]);
						tex.Add(t.TexCoords[0]);
						tex.Add(t.TexCoords[1]);
						tex.Add(t.TexCoords[2]);
						color.Add(t.Color.R/255.0);
						color.Add(t.Color.G / 255.0);
						color.Add(t.Color.B / 255.0);
					}
				}

				if (pos.Count == 0)
					continue;

				var baseName = "model" + i;
				var node = scene.CreateNode(baseName + "-node", baseName);

				instance_geometry instGeom = new instance_geometry() { };
				node.instance_geometry = new instance_geometry[] { instGeom };

				var geom = doc.CreateGeometry(baseName + "-mesh", baseName);
				instGeom.url = "#" + geom.id;
				var mesh = geom.CreateMesh();

				var positionsSource = mesh.CreatePositionVertices(geom.id + "-vertices", pos.ToArray());
				var normals = mesh.CreateXYZSource(geom.id + "-normals", nor.ToArray());
				var colorSrc = mesh.CreateRGBSource(geom.id + "-color", color.ToArray());
				var map1 = mesh.CreateSTPSource(geom.id + "-map-channel-1", tex.ToArray());

				instGeom.bind_material = new bind_material() {};
				instGeom.bind_material.technique_common = new instance_material[faceByMap.Keys.Count];
				int j = 0;
				int offset = 0;
				foreach (var faceMap in faceByMap)
				{
					if (faceMap.Value.triangleStream.Count == 0)
						continue;
					
					instGeom.bind_material.technique_common[j] = new instance_material() { symbol = "texture" + faceMap.Key, target = "#" + materials[faceMap.Key].id };
					var triangles = mesh.CreateTriangles("texture"+faceMap.Key);

					//triangles.CreateVertexInput(positionsSource, 0);
					//int[] idx = new int[faceMap.Value.triangleStream.TriangleCount];
					//for (int ii = 0; ii < idx.Length; ++ii)
					//{
					//    idx[ii] = offset + ii;
					//}

					triangles.CreateVertexInput(positionsSource, 0);
					triangles.CreateNormalInput(normals, 1);
					triangles.CreateColorInput(colorSrc, 2, 0);
					triangles.CreateTexCoordInput(map1, 3, 1);
					int stride = 4;
					int[] idx = new int[faceMap.Value.triangleStream.Count * stride];
					for (int ii = 0; ii < idx.Length; ++ii)
					{
						idx[ii] = offset + ii / stride;
					}
					triangles.CreateIndexArray(idx);
					triangles.count = (ulong)faceMap.Value.triangleStream.Count / 3;
					offset += faceMap.Value.triangleStream.Count;
					j=j+1;
				}


				//float[] pos = new float[model.n_faces*?];
				//var positions = mesh.CreatePositionsSource();
			}
			return doc;
		}
class ClusterComparer: IComparer<int>
{
	private IList<SimpleMeshGeometry> clusters;
	public ClusterComparer(IList<SimpleMeshGeometry> clusters)
	{
		this.clusters = clusters;
	}

	/// <summary>
	/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
	/// </summary>
	/// <returns>
	/// Value Condition Less than zerox is less than y.Zerox equals y.Greater than zerox is greater than y.
	/// </returns>
	/// <param name="y">The second object to compare.</param>
	/// <param name="x">The first object to compare.</param>
	public int Compare(int x, int y)
	{
		return clusters[x].VertexBuffer.Count - clusters[y].VertexBuffer.Count;
	}

}
		public COLLADA GetColladaFileLeafs(string dstFileName)
		{
			var doc = NewColladaFile();
			var scene = doc.library_visual_scenes.visual_scene[0];

			List<SimpleMeshGeometry> clusterGeometry = new List<SimpleMeshGeometry>(this.VisdataInfo.n_vecs);
			List<Set<int>> optimizedClusters = new List<Set<int>>();
			for (int i = 0; i < this.VisdataInfo.n_vecs; i++)
			{
				clusterGeometry.Add(new SimpleMeshGeometry());
				optimizedClusters.Add(new Set<int>());
			}


			for (int i = 0; i < LeafInfo.Length; i++)
			{
				var leaf = LeafInfo[i];

				//var meshGeometry = new SimpleMeshGeometry();

				if (leaf.cluster == -1)
					continue;
				
				var meshGeometry = clusterGeometry[leaf.cluster];

				for (int f = leaf.leafface; f < leaf.leafface + leaf.n_leaffaces; ++f)
				{
					var face = FaceInfo[LeaffacesInfo[f]];
					var mesh = meshGeometry.SubMeshes[doc.library_materials.material[face.texture]];

					switch (face.type)
					{
						case FaceType.polygon:
						case FaceType.mesh:
							AddPolyToMesh(face, mesh);
							break;
						case FaceType.patch:
							AddPatchToMesh(face, mesh);
							break;
					}
				}
			}

			// ------------- ОПТИМИЗИРУЕМ -----------------

			for (int i = 0; i < clusterGeometry.Count; i++)
			{
				for (int j = 0; j < clusterGeometry.Count; j++)
				{
					if (IsClusterVisisble(i, j))
					{
						optimizedClusters[i].Add(j);
					}
				}
			}
			again:
			int maxDIPs = 60;
			Set<int> setToOptimize = null;
			foreach (var set in optimizedClusters)
			{
				if (set.Count > maxDIPs)
				{
					maxDIPs = set.Count;
					setToOptimize = set;
				}
			}
			if (setToOptimize != null)
			{
				List<int> orderedClusters = new List<int>();
				foreach (int clusterId in setToOptimize)
				{
					orderedClusters.Add(clusterId);
				}
				orderedClusters.Sort(new ClusterComparer(clusterGeometry));
				for (int idSrcCluster = 0; idSrcCluster < orderedClusters.Count-1; ++idSrcCluster)
					for (int idDstCluster = idSrcCluster + 1; idDstCluster < orderedClusters.Count; ++idDstCluster)
					{
						if (clusterGeometry[idSrcCluster].SubMeshes.HaveCommonKeysWith(clusterGeometry[idDstCluster].SubMeshes))
						{
							Debug.WriteLine("merge " + idSrcCluster + " and " + idDstCluster);
							clusterGeometry[idSrcCluster].Merge(clusterGeometry[idDstCluster]);
							clusterGeometry.RemoveAt(idDstCluster);

							foreach (var oc in optimizedClusters)
							{
								var ar = oc.ToArray();
								oc.Clear();
								foreach (var item in ar)
								{
									if (item == idDstCluster)
										oc.Add(idSrcCluster);
									else if (item > idDstCluster)
										oc.Add(item-1);
									else
										oc.Add(item);
								}
							}
							goto again;
						}
					}
			}

			for (int i = 0; i < optimizedClusters.Count; i++)
			{
				Debug.WriteLine("optimized DIPs for "+i+" is "+optimizedClusters[i].Count);
			}
			// ------ СОЗДАЁМ ГЕОМЕТРИЮ
			for (int i = 0; i < clusterGeometry.Count; i++)
			{
				var meshGeometry = clusterGeometry[i];

				if (meshGeometry.VertexBuffer.Count == 0)
					continue;

				var baseName = "model" + i;
				var node = scene.CreateNode(baseName + "-node", baseName);

				instance_geometry instGeom = new instance_geometry() { };
				node.instance_geometry = new instance_geometry[] { instGeom };

				var geom = doc.CreateGeometry(baseName + "-mesh", baseName);
				instGeom.url = "#" + geom.id;
				var gmesh = geom.CreateMesh();

				var vertexBuffer = meshGeometry.VertexBuffer;
				
				var positionsSource = gmesh.CreatePositionVertices(geom.id + "-vertices", vertexBuffer.PositionXYZArray());
				var normals = gmesh.CreateXYZSource(geom.id + "-normals", vertexBuffer.NormalXYZArray());
				var colorSrc = gmesh.CreateRGBSource(geom.id + "-color", vertexBuffer.ColorRGBArray());
				var map1 = gmesh.CreateSTPSource(geom.id + "-map-channel-1", vertexBuffer.TexCoord0XYZArray());

				instGeom.bind_material = new bind_material() { };
				instGeom.bind_material.technique_common = new instance_material[meshGeometry.SubMeshes.Count];

				for (int subMeshId=0; subMeshId<meshGeometry.SubMeshes.Count; ++subMeshId)
				{
					var faceMap = meshGeometry.SubMeshes.GetIndexBuffer(subMeshId);
					var key = meshGeometry.SubMeshes.GetKey(subMeshId);
				

					instGeom.bind_material.technique_common[subMeshId] = new instance_material() { symbol = "texture" + subMeshId, target = "#" + ((material)key).id };
					var triangles = gmesh.CreateTriangles("texture" + subMeshId);

					triangles.CreateVertexInput(positionsSource, 0);
					triangles.CreateNormalInput(normals, 0);
					triangles.CreateColorInput(colorSrc, 0, 0);
					triangles.CreateTexCoordInput(map1, 0, 1);
					triangles.CreateIndexArray(faceMap.ToTriangleIndexBufferArray());
					triangles.count = (ulong)faceMap.TriangleCount;
				}

			}

			Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
			Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
			using (var js = File.CreateText(Path.ChangeExtension(dstFileName,".js")))
			{
				js.WriteLine("private var models:Array = new Array();");
				js.WriteLine("private var PVS:Array = new Array();");
				js.WriteLine("private var prevCluster = -1;");

js.WriteLine("function Start()");
js.WriteLine("{");
				for (int i = 0; i < optimizedClusters.Count; i++)
				{
					js.Write("	PVS[" + i + "] = [");
					string sep = "";
					foreach (int set in optimizedClusters[i])
					{
						js.Write(sep);
						js.Write(set);
						sep = ",";
					}
					js.WriteLine("];");
				}
				for (int i = 0; i < clusterGeometry.Count; i++)
				{
					var meshGeometry = clusterGeometry[i];
					js.WriteLine("	models["+i+"] = GameObject.Find(\"model" + i + "_node\");");
					js.WriteLine("	if (models[" + i + "]) models[" + i + "] = models[" + i + "].GetComponent(MeshRenderer);");
					js.WriteLine("	if (models[" + i + "]) models[" + i + "].enabled = false;");
				}
js.WriteLine("}");
js.WriteLine("function EnableCluster(i): void ");
js.WriteLine("{");
js.WriteLine("Debug.Log(\"enable \" + i);");
js.WriteLine("if (prevCluster == i)");
js.WriteLine("	return;");

js.WriteLine("if (prevCluster >= 0)");
js.WriteLine("	for (var j = 0; j < PVS[prevCluster].Length; ++j)");
js.WriteLine("		if (models[PVS[prevCluster][j]]) models[PVS[prevCluster][j]].enabled = false;");

js.WriteLine("prevCluster = i;");

js.WriteLine("if (i >= 0)");
js.WriteLine("	for (j=0; j<PVS[i].Length; ++j)");
js.WriteLine("		if (models[PVS[i][j]]) models[PVS[i][j]].enabled = true;");
js.WriteLine("}");
				js.WriteLine("function FixedUpdate () : void ");
//js.WriteLine("function Update()");
js.WriteLine("{");
js.WriteLine("var c = GameObject.Find(\"model0_node\").transform.InverseTransformPoint (Camera.main.transform.position);");
				js.WriteLine("c = Vector3(-c.x, c.y, c.z);");
				var min = MinVertex;
				var max = MaxVertex;
				var center = (min + max) * 0.5;
				js.WriteLine("c = c+Vector3(" + center.X + "," + center.Y + "," + center.Z + ");");

RenderBSP(js, 0);
js.WriteLine("if (guiText) guiText.text = \"CLUSTER \"+prevCluster+\" @ \"+c;");
js.WriteLine("}");

			}

			doc.Save(dstFileName);
			return doc;
		}

		private void RenderBSP(StreamWriter js, int i)
		{
			if (i < 0)
			{
				var c = LeafInfo[-(i+1)].cluster;
				js.WriteLine("EnableCluster(" + c + ");");
				return;
			}
			var plane1 = NodeInfo[i].plane;
			var plane = PlaneInfo[plane1];

			js.WriteLine("if (Vector3.Dot(c,Vector3(" + plane.normal[0] + "," + plane.normal[1] + "," + plane.normal[2] + ")) - "+plane.dist+" >= 0)");
			js.WriteLine("{");
			RenderBSP(js, NodeInfo[i].children[0]);
			js.WriteLine("}");
			js.WriteLine("else");
			js.WriteLine("{");
			RenderBSP(js, NodeInfo[i].children[1]);
			js.WriteLine("}");
		}

		private bool IsClusterVisisble(int i, int j)
		{
			return 0 != (VisdataInfo.vecs[i*VisdataInfo.sz_vecs+j/8] & (1 << (j%8)));
		}
		Vector3d MaxVertex
		{
			get
			{
				double x = double.MinValue;
				double y = double.MinValue;
				double z = double.MinValue;
				foreach (var v in this.VertexInfo)
				{
					if (v.position[0] > x)
						x = v.position[0];
					if (v.position[1] > y)
						y = v.position[1];
					if (v.position[2] > z)
						z = v.position[2];
				}
				return new Vector3d(x, y, z);
			}
		}
		Vector3d MinVertex
		{
			get
			{
				double x = double.MaxValue;
				double y = double.MaxValue;
				double z = double.MaxValue;
				foreach (var v in this.VertexInfo)
				{
					if (v.position[0] < x)
						x = v.position[0];
					if (v.position[1] < y)
						y = v.position[1];
					if (v.position[2] < z)
						z = v.position[2];
				}
				return new Vector3d(x,y,z);
			}
		}
		private void AddPatchToMesh(Face face, IndexBuffer mesh)
		{
			var width = face.size[0];
			var height = face.size[1];
			if (width * height != face.n_vertexes)
				throw new BSPFormatException("wrong patch point count");

			if ((width & 1) == 0)
				throw new BSPFormatException("path should be 3x3");

			if ((height & 1) == 0)
				throw new BSPFormatException("path should be 3x3");

			for (int i = 0; i < width - 1; i += 1)
			{
				for (int j = 0; j < height - 1; j += 1)
				{
					mesh.QuadTriangle(
							CreateVertex(VertexInfo[face.vertex + (i) + (j) * width]),
							CreateVertex(VertexInfo[face.vertex + (i+1) + (j) * width]),
							CreateVertex(VertexInfo[face.vertex + (i+1) + (j+1) * width]),
							CreateVertex(VertexInfo[face.vertex + (i) + (j+1) * width]));
				}
			}
		}

		private void AddPolyToMesh(Face face,  IndexBuffer mesh)
		{
			for (int i = 0; i < face.n_meshverts / 3; ++i)
			{
				mesh.AddTriangle(
						CreateVertex(VertexInfo[MeshvertsInfo[face.meshvert + i * 3 + 2] + face.vertex]),
						CreateVertex(VertexInfo[MeshvertsInfo[face.meshvert + i * 3 + 1] + face.vertex]),
						CreateVertex(VertexInfo[MeshvertsInfo[face.meshvert + i * 3 + 0] + face.vertex]));
			}
		}

		public SimpleMesh.Vertex CreateVertex(Vertex srcVert)
		{
			return new SimpleMesh.Vertex
			        	{
			        			Position = new Vector3d(srcVert.position[0], srcVert.position[1], srcVert.position[2]),
			        			Normal = new Vector3d(srcVert.normal[0], srcVert.normal[1], srcVert.normal[2]),
								TexCoord0 = {TexCoord = new Vector3d(srcVert.texCoords[0], 1 - srcVert.texCoords[1], 0)},
			        			ColorValue = srcVert.color
			        	};
		}
	}
}